前兩天用模組建構的角度來探討import的機制,現在我們就一個使用者的角度來思考究竟python的import應該俱備什麼功能。
議題一:今天我想要去import別人寫好的一個module,但他不存在當下的工作目錄底下,那我應該有什麼方法可以得到這個module呢?
python其實有很多個方法可以達成這件事,第一個最簡單的方法就是用把欲加入的module的路徑手動加到sys.path這個list裏面:
In python3 shell:
>>> import sys
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']
從印出sys.path就可以知道平時python在load module的時候是從哪裡尋找module的位置,一般的操作指令都可以很簡單的手動加入新的路徑到sys.path裏面:
sys.path.insert(0, 'some path')
sys.path.append('some path')
sys.path.extend(['some path','some path'....])
但這方法的缺點在於我們會把路徑寫死在程式碼裏面,當我們把這個被引入的模組更換一下路徑,那所有寫死路徑的程式碼都要被叫出來改掉,萬一這個模組有剛好是很通用的模組,被一堆不同部份的code所import,那真的是改路徑改到人仰馬翻。
既然在程式碼中加入module可能會遇到這種麻煩的問題,那只能訴諸程式碼外的解決方式了。
其中一個是利用設定PYTHONPATH的方式來新增尋找module的路徑:
In bash:
$ env PYTHONPATH='/home/shnovaj30101' python3
Python 3.4.3 (default, Nov 17 2016, 01:08:31)
[GCC 4.8.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/home/shnovaj30101', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']
>>>
當我們在呼叫python的程式之前,先將'/home/shnovaj30101'這個路徑加入環境變數PYTHONPATH中,然後就會發現sys.path已經把'/home/shnovaj30101'這個路徑放在裏面了!
還有另外一個方法就是把路徑放在一個.pth檔裏面,這個.pth檔的作用呢,就是在python初始化時,為sys.path添加額外的變數,那這個.pth檔要放在哪裡呢?
In python3 shell:
>>> import sys
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/usr/lib/python3/dist-packages']
通常都會把pth檔加在site-python/或是site-packages/裏面,但是我的路徑好像沒有這些資料夾,看了一下source code裏面發現dist-packages裏面也可,所以現在就隨便建一個測試的pth檔吧:
In test.pth:
/home/shnovaj30101
然後把這個test.pth檔加進'/usr/local/lib/python3.4/dist-packages'裏面。
>>> import sys
>>> sys.path
['', '/usr/lib/python3.4', '/usr/lib/python3.4/plat-x86_64-linux-gnu', '/usr/lib/python3.4/lib-dynload', '/usr/local/lib/python3.4/dist-packages', '/home/shnovaj30101', '/usr/lib/python3/dist-packages']
>>>
果不其然,'/home/shnovaj30101'因為我剛剛寫的pth檔,所以在初始化的時候被加進去了。
議題二(充字數again):來講一下議題一的原理吧,到底python的初始化過程式如何設定sys.path呢?
在python初始化的過程中,第一個被加進sys.path的元素是一個空字串'',這個空字串指的是呼叫python當下所在的工作目錄,所以放置在工作目錄的模組一定可以被import進程式當中。
接著就是我們議題一的第二個方法,python會讀取環境變數PYTHONPATH所指定的路徑名稱,從議題一的範例也可以證明,PYTHONPATH內含的路徑名稱,是第二個被加進sys.path的路徑,因為他們就正好就位於空字串的後面。
第三個被引入的路徑,會由python的site模組來分配,同時我們的pth檔之所以能夠新增路徑到sys.path,也是因為site模組的關係,那如果我們要更深入研究site模組是如何分配路徑,我們必須先找到他的原始碼site.py,然後看看他是怎麼實作的:
In python3 shell:
>>> import site
>>> site # 尋找site.py的位置
<module 'site' from '/usr/lib/python3.4/site.py'>
>>>
經過網路查閱得知當site模組被import進來就會自己添加路徑到sys.path,那我們要先從'/usr/lib/python3.4/site.py'中尋找當site.py被import時會做出什麼行為:
In /usr/lib/python3.4/site.py (line 559):
def main():
"""Add standard site-specific directories to the module search path.
This function is called automatically when this module is imported,
unless the python interpreter was started with the -S flag.
"""
global ENABLE_USER_SITE
abs_paths()
known_paths = removeduppaths()
known_paths = venv(known_paths)
if ENABLE_USER_SITE is None:
ENABLE_USER_SITE = check_enableusersite()
known_paths = addusersitepackages(known_paths)
known_paths = addsitepackages(known_paths)
setquit()
setcopyright()
sethelper()
enablerlcompleter()
aliasmbcs()
execsitecustomize()
if ENABLE_USER_SITE:
execusercustomize()
# Prevent edition of sys.path when python was started with -S and
# site is imported later.
if not sys.flags.no_site:
main()
在site.py裏面,幾乎所有程式碼都是函式定義,唯一會呼叫的就是上面代碼中最下面的兩行程式,所以我們知道了當site.py被import進來時最先被呼叫的入口函式就是main()。
礙於篇幅不夠所以我只挑重要的講,基本上每個function最前面都有一個註釋,看了註釋覺得和sys.path最相關的應該就是addusersitepackages和addsitepackages了:
In /usr/lib/python3.4/site.py (line 256):
def getusersitepackages():
"""Returns the user-specific site-packages directory path.
If the global variable ``USER_SITE`` is not initialized yet, this
function will also set it.
"""
global USER_SITE
user_base = getuserbase() # this will also set USER_BASE
if USER_SITE is not None:
return USER_SITE
from sysconfig import get_path
if sys.platform == 'darwin':
from sysconfig import get_config_var
if get_config_var('PYTHONFRAMEWORK'):
USER_SITE = get_path('purelib', 'osx_framework_user')
return USER_SITE
USER_SITE = get_path('purelib', '%s_user' % os.name)
return USER_SITE
def addusersitepackages(known_paths):
"""Add a per user site-package to sys.path
Each user has its own python directory with site-packages in the
home directory.
"""
# get the per user site-package path
# this call will also make sure USER_BASE and USER_SITE are set
user_site = getusersitepackages()
if ENABLE_USER_SITE and os.path.isdir(user_site):
addsitedir(user_site, known_paths)
if ENABLE_USER_SITE:
for dist_libdir in ("lib", "local/lib"):
user_site = os.path.join(USER_BASE, dist_libdir,
"python" + sys.version[:3],
"dist-packages")
if os.path.isdir(user_site):
addsitedir(user_site, known_paths)
return known_paths
In /usr/lib/python3.4/site.py (line 300):
def getsitepackages(prefixes=None):
"""Returns a list containing all global site-packages directories
(and possibly site-python).
For each directory present in ``prefixes`` (or the global ``PREFIXES``),
this function will find its `site-packages` subdirectory depending on the
system environment, and will return a list of full paths.
"""
sitepackages = []
seen = set()
if prefixes is None:
prefixes = PREFIXES
for prefix in prefixes:
if not prefix or prefix in seen:
continue
seen.add(prefix)
if os.sep == '/':
if 'VIRTUAL_ENV' in os.environ or sys.base_prefix != sys.prefix:
sitepackages.append(os.path.join(prefix, "lib",
"python" + sys.version[:3],
"site-packages"))
sitepackages.append(os.path.join(prefix, "local/lib",
"python" + sys.version[:3],
"dist-packages"))
sitepackages.append(os.path.join(prefix, "lib",
"python3",
"dist-packages"))
# this one is deprecated for Debian
sitepackages.append(os.path.join(prefix, "lib",
"python" + sys.version[:3],
"dist-packages"))
sitepackages.append(os.path.join(prefix, "lib", "dist-python"))
else:
sitepackages.append(prefix)
sitepackages.append(os.path.join(prefix, "lib", "site-packages"))
if sys.platform == "darwin":
# for framework builds *only* we add the standard Apple
# locations.
from sysconfig import get_config_var
framework = get_config_var("PYTHONFRAMEWORK")
if framework:
sitepackages.append(
os.path.join("/Library", framework,
sys.version[:3], "site-packages"))
return sitepackages
def addsitepackages(known_paths, prefixes=None):
"""Add site-packages (and possibly site-python) to sys.path"""
for sitedir in getsitepackages(prefixes):
if os.path.isdir(sitedir):
if "site-python" in sitedir:
import warnings
warnings.warn('"site-python" directories will not be '
'supported in 3.5 anymore',
DeprecationWarning)
addsitedir(sitedir, known_paths)
return known_paths
看起來落落長阿,沒關係且聽我明天細細說明,不想在一天中塞太多內容,這樣只會讓我自己累死XD。
先來說明一下看source code的心法,其實沒什麼,就是一個懶字而已,切記當一個source code牽涉到的東西比較複雜時,很多東西能忽略就忽略,能假設就假設,不要一次把他全看完,注意對自己重要的東西就好。
與其辛苦的把他從頭讀完,一次就讀到懂,不如只看重要的東西,然後看很多次,發現還是有不懂的地方,就在看細一點,這樣比較不會喪失焦點,也不會太耗腦力,更能省時間。